﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MessageServer.utilities;
using System.Reflection;
using System.Collections;
using MessageServer.Messages;

namespace MessageServer.serialize
{
    /**
     * This class formats the object into a binary byte stream and deserializes
     * the binary byte stream into an object. In the binary serializing format, this
     * class formats basic data types using the ObjectFormatter. This current implementation
     * is the little endian format. For any custom data types this function adds the length of
     * the object as the first two bytes, then adds all the data to the rest. For strings, it
     * adds the length of the string before adding the data of the string. This serialization
     * format assumes that both the sender and the receiver have the same data definition
     * and does not contain any data definitions at all.
     * */
    public class BinarySerializer : SerializeFormat
    {
        Formatter _formatter;           //The formatter that will be used to format primary data types.

        //will be initialized to use the littleendian formatting
        public BinarySerializer()
        {
            _formatter = new Formatter(true); //BNG uses lsb first
        }

        //returns true if the type passed is a serializable object. This checks
        //if the class has the MsgSerializable attribute defined. 
        private bool isSerializable(Type t)
        {
            return t.IsDefined(typeof(MsgSerializable), false); //has to be declared in the current class
        }

        //This function returns true if the formatter of this class
        //supports serializing the given type.
        private bool canFormat(Type t)
        {
            return _formatter.supports(t);
        }

        //This function checks if the type passed contains the AddLength property.
        //if it does, then this function calls the property on the passed object
        //to determine if the length has to be added, else it returns true. By default
        //the length is added to the serializing bytes.
        private bool shouldAddLen(Type t, object obj)
        {
            bool addlen = true; //by default add
            PropertyInfo info = t.GetProperty("AddLength");
            if (info != null)
            {
                addlen = (bool)info.GetValue(obj, null);
            }
            return addlen;
        }

        /// <summary>
        /// This function retreives all the fields of all the base classes as well as this class
        /// and returns then in the order of the base classes first, followed by the fields of this
        /// class.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private List<FieldInfo> getFields(Type t)
        {
            List<FieldInfo> flds = new List<FieldInfo>();
            if (t != null)
            {
                Type btype = t.BaseType;
                if ((btype != null) && !btype.Name.Equals("Object"))
                {
                    List<FieldInfo> bflds = getFields(t.BaseType);
                    flds.AddRange(bflds);
                } 

                FieldInfo[] vals = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                foreach (FieldInfo fld in vals)
                    flds.Add(fld);

            }
            return flds;
        }

        /// <summary>
        /// This function determines the length of the given field in the given object and field type passed.
        /// </summary>
        /// <param name="fldtype"></param>
        /// <param name="obj"></param>
        /// <returns>the length of the serialized bytes for the object obj</returns>
        private int fldLen(Type fldtype, object obj)
        {
            int len = 0;
            fldtype = obj.GetType();
            if (canFormat(fldtype))
            {
                //handle this differently
                ObjectFormatter format = _formatter.formatterfor(fldtype);
                len += format.Length(obj);
            }
            else if (typeof(ICollection).IsAssignableFrom(fldtype))
            {
                len += ListLen(obj as ICollection);
            }
            else if (isSerializable(fldtype))
            {
                if (obj != null)
                {
                    len += Length(obj);
                }
                else
                    len += _formatter.UINT16.Length(null); //write 0 length
            }
            else
                throw new Exception("Cannot serialize object");

            return len;
        }

        /// <summary>
        /// This function determines the length of the list passed to the function when
        /// serialized.
        /// </summary>
        /// <param name="coll"></param>
        /// <returns>the length of the bytes that will be created when this list is serialized</returns>
        private int ListLen(ICollection coll)
        {
            int len = _formatter.UINT16.Length(null);
            foreach (object o in coll)
            {
                Type t = o.GetType();
                len += fldLen(t, o);
            }

            return len;
        }

        /**
         * This returns the total length of the object obj when serialized. This uses the helper functions
         * listlength and field length to calculate the length.
         * */
        public int Length(object obj)
        {
            int len = 0;
            Type t = obj.GetType();
            if (isSerializable(t))
            {
                bool addlen = shouldAddLen(t, obj);
                if (addlen) len += 2;
                List<FieldInfo> flds = getFields(t);
                foreach (FieldInfo fld in flds)
                {
                    Type fldtype = fld.FieldType;
                    len += fldLen(fldtype, fld.GetValue(obj));
                }
            }
            else
                throw new Exception("Cannot serialize object");

            return len;
        }

        /**
         * This function formats the given field in the object val. This function
         * returns the serialized bytes of the given field. If it is a list, this function
         * calls the formatlist function to serialize it, if it is a primitive type, this
         * function uses the formatter to serialize it and if it is another object it
         * calls the serialize function to serialize it.
         * */
        private byte[] formatFld(Type fldtype, object val)
        {
            byte[] thisfld = null;
            fldtype = val.GetType();
            if (canFormat(fldtype))
            {
                //handle this differently
                thisfld = formatbasic(fldtype, val);
            }
            else if (typeof(ICollection).IsAssignableFrom(fldtype))
            {
                if (fldtype.IsGenericType)
                {
                    //is a list to handle it differently
                    ICollection coll = (ICollection)val;
                    thisfld = formatList(coll);
                }
                else
                    throw new NotSupportedException(); //if it is a object list with no template we don't support
            }
            else
            {
                if (isSerializable(fldtype))
                {
                    //invoke serializable here
                    if (val != null)
                        thisfld = serialize(val);
                    else
                    {
                        //set to a 0 sized object??
                        thisfld = new byte[2];
                        thisfld[0] = 0;
                        thisfld[1] = 0;
                    }
                }
                else
                    throw new Exception("Cannot have non-serializable members");
            }

            return thisfld;
        }

        /**
         * This function is a helper function to format the basic or primitive format
         * of field fldtype and represented by val. This calls the formatter function
         * to format the data.
         * */
        private byte[] formatbasic(Type fldtype, object val)
        {
            byte[] thisfld = null;
            ObjectFormatter format = _formatter.formatterfor(fldtype);
            if (format != null)
                thisfld = format.serialize(val);
            else
                throw new Exception("BNGSerializer:serialize:Not supported type currently." + fldtype.Name);

            return thisfld;
        }

        /**
         * Formats the list of the collection data in coll variable passed. It calls the formatfld
         * function to format the list.
         * */
        private byte[] formatList(ICollection coll)
        {
            ByteBuffer lstbuff = new ByteBuffer(10);
            //assumption is that the collection is a homogeneous collection the type of the
            //first element is the type of all elements.
            UInt16 cnt = (UInt16)coll.Count;
            byte[] sz = _formatter.UINT16.serialize(cnt);
            lstbuff.Append(sz, 0, sz.Length);
            if (cnt > 0)
            {
                foreach (object o in coll)
                {
                    Type t = o.GetType();
                    byte[] one = formatFld(t, o);
                    lstbuff.Append(one, 0, one.Length);
                }
            }

            return lstbuff.getData();
        }

        /** 
         * will serialize all fields from the base and current object. Does not expect
         * any field declared to be non-serializable. All messages to be
         * sent has to be in a data class by itself and not combined with
         * any functionality.
         * */
        public byte[] serialize(object obj)
        {
            Type t = obj.GetType();
            ByteBuffer bytes = new ByteBuffer(10);
            if (isSerializable(t))
            {
                bool addlen = shouldAddLen(t, obj);
                byte[] blen = new byte[_formatter.UINT16.Length(null)];
                blen[0] = 0; blen[1] = 0; //set it to 0 initially
                //reserve place for len
                if (addlen) bytes.Append(blen, 0, blen.Length);
                List<FieldInfo> flds = getFields(t);
                foreach (FieldInfo fld in flds)
                {
                    byte[] thisfld = null;
                    Type fldtype = fld.FieldType;
                    thisfld = formatFld(fldtype, fld.GetValue(obj));
                    bytes.Append(thisfld, 0, thisfld.Length);
                }

                if (addlen)
                {
                    UInt16 len = (UInt16)(bytes.Length - _formatter.UINT16.Length(null)); //exclude the two bytes of the length
                    blen = _formatter.UINT16.serialize(len);
                    bytes.Replace(blen, 0, _formatter.UINT16.Length(null));
                }
            }

            return bytes.getData();
        }

        /**
         * This function shortens the original bytes by len amount.
         * */
        public byte[] shorten(byte[] original, int len)
        {
            if (len <= 0) return original;

            byte[] ret = new byte[original.Length - len];
            for (int i = len; i < original.Length; i++)
            {
                ret[i - len]  = original[i];
            }

            return ret;
        }

        /**
         * Helper function to create an object of type t using the ObjectConstructor 
         * passed to the function.
         * */
        private object defaultconstruct(ObjectConstructor construct, Type t)
        {
            /*
             * ConstructorInfo construct = t.GetConstructor(new Type[0]);
            return construct.Invoke(new object[0]);*/
            return construct.construct(t);
        }

        /**
         * This function creates the list object from a set of bytes passed to this function. 
         * It returns the length of the bytes deserialized in the len parameter, it uses the
         * fldtype function to determine the object in the list and the construct parameter
         * to create each object in the list.
         * */
        private object createlst(ObjectConstructor construct, Type fldtype, byte[] bytes, ref int len)
        {
            object coll = defaultconstruct(construct, fldtype);
            Type[] objtype = fldtype.GetGenericArguments();
            MethodInfo mthd = fldtype.GetMethod("Add", objtype);

            if (objtype.Length <= 0) throw new NotSupportedException();
            int cntlen = 0;
            UInt16 cnt = (UInt16)_formatter.UINT16.deserialize(bytes, ref cntlen);
            byte[] restbytes = shorten(bytes, cntlen);
            len += _formatter.UINT16.Length(null);
            for (int i = 0; i < cnt; i++)
            {
                int onelen = 0;
                object oneval = deserializefld(construct, objtype[0], restbytes, ref onelen);
                len += onelen;
                restbytes = shorten(restbytes, onelen);
                mthd.Invoke(coll, new object[] { oneval });
            }

            return coll;
        }

        /**
         * This function deserializes the field represented by the field type in the bytes and
         * returns the length of deserialized bytes in the len parameter.
         * */
        private object deserializefld(ObjectConstructor construct, Type fldtype, byte[] bytes, ref int len)
        {
            object val = null;
            int thislen = 0;
            if (canFormat(fldtype))
            {
                ObjectFormatter format = _formatter.formatterfor(fldtype);
                if (format != null)
                {
                    val = format.deserialize(bytes, ref thislen);
                }
                else
                    throw new Exception("BNGSerializer:deserialize: Not supported type: " + fldtype.Name);
            }
            else if (typeof(ICollection).IsAssignableFrom(fldtype))
            {
                if (fldtype.IsGenericType)
                {
                    val = createlst(construct, fldtype, bytes, ref thislen);
                }
                else
                    throw new NotSupportedException(); //cannot do if it is not the generic one
            }
            else
            {
                val = deserialize(bytes, fldtype, construct, ref thislen);
            }

            len += thislen;
            return val;
        }

        /**
         * This fuction deserializes the data in the bytes parameter into a type represented by t. It uses
         * the construct parameter to create the objects and returns the length deserialized in the dlen
         * parameter.
         * */
        public object deserialize(byte[] bytes, Type t, ObjectConstructor construct, ref int dlen)
        {
            object ret = null;
            byte[] restbytes = bytes;
            ret = defaultconstruct(construct, t);
            if (ret != null) t = ret.GetType(); //for objects reset the type to the constructed object
            if (isSerializable(t))
            {
                bool addlen = shouldAddLen(t, ret);
                int lenlen = 0;
                if (addlen)
                {
                    UInt16 olen = (UInt16)_formatter.UINT16.deserialize(bytes, ref lenlen);
                    restbytes = shorten(restbytes, lenlen); //now shd this be used??
                    dlen += lenlen;
                    if (restbytes.Length < olen) throw new PartialMessageException(); //ensure we have the correct length
                }
                List<FieldInfo> flds = getFields(t);
                foreach (FieldInfo fld in flds)
                {
                    object val = null;
                    Type fldtype = fld.FieldType;
                    int thislen = 0;
                    val = deserializefld(construct, fldtype, restbytes, ref thislen);
                    fld.SetValue(ret, val);
                    dlen += thislen;
                    restbytes = shorten(restbytes, thislen);
                }
            }
            else
                throw new Exception("Cannot have non-serializable members");

            return ret;
        }
    }
}
